package proxy;


import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;

import html.HttpClient;
import html.HttpClientPipelineFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Executor;
import javax.net.ssl.SSLEngine;

import misc.CLoggerFactory;

import org.apache.log4j.Logger;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.http.Cookie;
import org.jboss.netty.handler.codec.http.CookieDecoder;
import org.jboss.netty.handler.codec.http.CookieEncoder;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpContentDecompressor;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.ssl.SslHandler;


import proxy.ProxyServer.EServerMode;
import securechat.SecureChatSslContextFactory;

public class ProxyRequestHandler extends SimpleChannelUpstreamHandler {

    private static final String PROXY_VERSION = "1.3";
	private static final String GENERIC_USER_AGENT = "Generic User Agent";

    private Executor _bossExecutor;
    private Executor _workerExecutor ;
    private HostResponseHandler _HostResponseHandler;
	private Logger ProxyRequestsAndResponesLog;
	private Logger log;
	private HttpRequest request;
	private String host;
	private int port;
    
    
    public ProxyRequestHandler(Executor bossExecutor, Executor workerExecutor ) {
		
		this._bossExecutor=bossExecutor;
	    this._workerExecutor=workerExecutor;
	    
	    try {
	    	this.log=CLoggerFactory.getFileLogger("ProxyRequestHandler", "ProxyRequestHandler.log") ;
			this.ProxyRequestsAndResponesLog = CLoggerFactory.getFileLogger("ProxyRequestsAndRespones", "ProxyRequestsAndRespones.log") ;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

    /**
     * Invoked when a message object (e.g: ChannelBuffer) was received from a remote peer.
     */
	@Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		
		final Channel ProxyChannel=ctx.getChannel();
    	this.request = (HttpRequest) e.getMessage();
    	SocketAddress LocalAddress =ctx.getChannel().getLocalAddress();
    	
       	ProxyRequestsAndResponesLog.info("Request From client address: " +
       									 LocalAddress + " ********* \n" +
       									 request + "\n");
    	
    			
    	takeCareOfRequestHeader(LocalAddress); 
    	retriveURIfromRequest();
    	ClientBootstrap bootstrap = configureTheClient();
    	
        log.info("Start the connection attempt  to host : " + host + " port : " + port);
        
        // Start the connection attempt.
        ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));

        //creating a listener
        final ChannelFutureListener lisnter=new ChannelFutureListener() {        	
			public void operationComplete(ChannelFuture future) throws Exception {
				
				AnswerAndClose(_HostResponseHandler.getResponseFromHost() ,ProxyChannel);
                        }
                   };
                   
        // Wait until the connection attempt succeeds or fails.
        future.addListener(new ChannelFutureListener()  {
        	
			public void operationComplete(ChannelFuture future) throws Exception {
				if (future.isSuccess()) {
					future.getChannel().getCloseFuture().addListener(lisnter);
                } 				
				else {
					future.getChannel().close(); 
					HttpResponse notSuccessfulResponse=new DefaultHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.NOT_FOUND);
					AnswerAndClose(notSuccessfulResponse,ProxyChannel);
				}
        	}
        });         	
    }

	/**
	 * Configuring The imaginary Client(the proxy server) to connect to the remote host
	 * @return
	 */
	private ClientBootstrap configureTheClient() {
		
		// Configure the client.
        ClientBootstrap bootstrap = new ClientBootstrap(
                new NioClientSocketChannelFactory(_bossExecutor,_workerExecutor));
       
        ChannelPipeline ChannelPipeline = bootstrap.getPipeline();
        
        _HostResponseHandler=new HostResponseHandler(request);   
        ChannelPipeline.addLast("decoder", new HttpResponseDecoder());        
        // Remove the following line if you don't want automatic content decompression.
        ChannelPipeline.addLast("inflater", new HttpContentDecompressor());        
        ChannelPipeline.addLast("aggregator", new HttpChunkAggregator(1048576));
        ChannelPipeline.addLast("encoder", new HttpRequestEncoder());
        ChannelPipeline.addLast("handler", _HostResponseHandler);        
        
		return bootstrap;
	}

	/**
	 * Retrieving the URI address from the Request
	 * @throws URISyntaxException
	 */
	private void retriveURIfromRequest()
			throws URISyntaxException {
		
		URI uri= new URI(request.getUri());		
    	String scheme = uri.getScheme() == null? "http" : uri.getScheme();
        host = uri.getHost() == null? "localhost" : uri.getHost();
        port = uri.getPort();

        if (port == -1)           
            port = 80;

        if (!scheme.equalsIgnoreCase("http") ) {
            System.err.println("Only HTTP is supported.");
            System.exit(1);
         }
	}

	/**
	 * Taking Care Of the Request Header considering in which mode the server is running 
	 * @param LocalAddress
	 */
	private void takeCareOfRequestHeader(SocketAddress LocalAddress) {
		switch (ProxyServer.serverMode) {
    	
	    	case EOpenMode:	           
	    		OpenMode(LocalAddress.toString());
	            break;
	            
	    	case EAnonymousMode:	           
	    		AnonymousMode();
	            break;
	            
            default:
            	OpenMode(LocalAddress.toString());
                break;
	    	
    	}
	}

	/**
	 * Sending the response that received from the remote host to the client and closing the channel 
	 * @param responseFromHost
	 * @param proxyChannel
	 */
	private void AnswerAndClose(HttpResponse responseFromHost, Channel proxyChannel ) {

    	ProxyRequestsAndResponesLog.info("Response From Host : ********* \n" +
    										responseFromHost);
    	responseFromHost.setHeader("Connection", "close");
    	proxyChannel.write(responseFromHost).addListener(ChannelFutureListener.CLOSE); 
	}


	/**
	 * 
	 * Taking Care Of the Request Header considering that the server is running in Anonymous Mode 
	 */
	private void AnonymousMode() {
		request.removeHeader(COOKIE);
		request.setHeader(USER_AGENT , GENERIC_USER_AGENT);
	}

	/**
	 * Taking Care Of the Request Header considering that the server is running in Open Mode 
	 * @param localAddress
	 */
	private void OpenMode(String localAddress) {
		int tIndex=localAddress.lastIndexOf(':');
		localAddress=localAddress.substring(1, tIndex) ;
		
		if( request.getHeader("X-Forwarded-For") != null){
			localAddress=request.getHeader("X-Forwarded-For")+ localAddress;
		}
		
    	request.addHeader("X-Forwarded-For",localAddress);
    	request.addHeader("proxy-version", PROXY_VERSION );  
	}
	

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
            throws Exception {
    	log.debug("exception Caught in ProxyRequestHandler " + e.getCause());
        e.getCause().printStackTrace();
        e.getChannel().close();
    }
    
 
}
